
package com.idealighter.game.games.xzdd.manager;

import com.google.common.collect.Multimap;
import com.google.common.eventbus.Subscribe;
import com.google.inject.Inject;
import com.google.inject.Singleton;

import com.idealighter.game.common.CommonMsgMgr;
import com.idealighter.game.core.common.Game;
import com.idealighter.game.core.constant.Operator;
import com.idealighter.game.core.error.ErrorCode;
import com.idealighter.game.core.tuple.Pair;
import com.idealighter.game.dblog.service.DbLogService;
import com.idealighter.game.dictionary.dic.XzddRoomDic;
import com.idealighter.game.dictionary.dic.XzddTimeDic;
import com.idealighter.game.dictionary.domain.XzddRoomDomain;
import com.idealighter.game.event.manager.EventMgr;
import com.idealighter.game.event.struct.ClearGameDataEvent;
import com.idealighter.game.event.struct.PlayerExitEvent;
import com.idealighter.game.event.struct.ShutdownRoomEvent;
import com.idealighter.game.event.struct.StartRoomEvent;
import com.idealighter.game.gamehall.event.EnterRoomEvent;
import com.idealighter.game.gamehall.event.EnterTableEvent;
import com.idealighter.game.gamehall.event.ExitRoomEvent;
import com.idealighter.game.gamehall.event.ExitTableEvent;
import com.idealighter.game.gamehall.event.RoomMemInfoUpdateEvent;
import com.idealighter.game.gamehall.manager.GameHallMgr;
import com.idealighter.game.games.common.AbstractRoom;
import com.idealighter.game.games.common.SeatState;
import com.idealighter.game.games.xzdd.dto.HuPaiItemInfo;
import com.idealighter.game.games.xzdd.struct.XzddGangType;
import com.idealighter.game.games.xzdd.struct.XzddGrabState;
import com.idealighter.game.games.xzdd.struct.XzddGrabType;
import com.idealighter.game.games.xzdd.struct.XzddHuPaiType;
import com.idealighter.game.games.xzdd.struct.XzddHuType;
import com.idealighter.game.games.xzdd.struct.XzddMj;
import com.idealighter.game.games.xzdd.struct.XzddMjType;
import com.idealighter.game.games.xzdd.struct.XzddPlayerBill;
import com.idealighter.game.games.xzdd.struct.XzddRoom;
import com.idealighter.game.games.xzdd.struct.XzddSeat;
import com.idealighter.game.games.xzdd.struct.XzddSeatCards;
import com.idealighter.game.games.xzdd.struct.XzddStage;
import com.idealighter.game.games.xzdd.struct.XzddTable;
import com.idealighter.game.games.xzdd.util.XzddCalculator;
import com.idealighter.game.games.xzdd.util.XzddCardsAnalyzer;
import com.idealighter.game.log.core.LogReason;
import com.idealighter.game.log.struct.game.GameAfeeLog;
import com.idealighter.game.login.manager.LoginMgr;
import com.idealighter.game.player.manager.PlayerMgr;
import com.idealighter.game.player.struct.Player;
import com.idealighter.game.schedule.manager.ScheduleMgr;
import com.idealighter.game.server.ExecutorMgr;
import com.idealighter.game.server.core.executor.DisruptorExecutor;
import com.idealighter.utils.code.RandCodeUtil;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 血战到底 .
 * 
 * @date 2016年9月2日 上午10:32:48
 *
 */
@Singleton
public class XzddMgr {

  private static final Logger LOG = LoggerFactory.getLogger(XzddMgr.class);

  // 普通玩家手上的牌数
  public static final int PLAYER_CARDS = 13;
  // 定时服务器增加的延时，因为与网络延时等原因
  private static final int ADDED_DELAY = 3;
  // 换牌最大数量
  private static final int EXCHANGE_MAX = 3;

  private final EventMgr eventMgr;
  // 游戏线程executor,可以执行普通任务和定时任务
  private final DisruptorExecutor gameExecutor;
  @Inject
  private XzddTimeDic timeDic;
  @Inject
  private XzddBiller biller;
  @Inject
  private PlayerMgr playerMgr;
  @Inject
  private ScheduleMgr scheduleMgr;
  @Inject
  private CommonMsgMgr commonMsgMgr;
  @Inject
  private XzddMsgMgr msgMgr;
  @Inject
  private XzddDataMgr dataMgr;
  @Inject
  private XzddRoomDic roomDic;
  @Inject
  private GameHallMgr gameHallMgr;
  @Inject
  private LoginMgr loginMgr;


  /**
   * 构造函数 .
   * 
   * @param eventMgr 事件管理.
   * @param executorMgr executor管理.
   */
  @Inject
  public XzddMgr(EventMgr eventMgr, ExecutorMgr executorMgr) {
    this.eventMgr = eventMgr;
    eventMgr.register(this);
    this.gameExecutor = executorMgr.getGameExecutor(Game.XZDD.getModuleId());
  }

  /**
   * 订阅玩家退出游戏事件,在gameExecutor中执行,避免线程安全问题 .
   * 
   * @param event .
   */
  @Subscribe
  public void onPlayerExitGame(PlayerExitEvent event) {
    gameExecutor.execute(() -> {
      Player player = event.player;
      long playerId = player.getId();

      XzddSeat seat = dataMgr.getPlayerSeat(playerId);
      if (seat != null) {
        doExitTabl(seat);
      }

      // 玩家是否在血战到底房间中
      if (player.curRoom instanceof XzddRoom) {
        // 退出房间
        exitRoom(player);
      }
    });
  }

  /**
   * 开始房间.
   * 
   * @param event 事件.
   */
  @Subscribe
  public void onStartRoom(StartRoomEvent event) {
    gameExecutor.execute(() -> {
      if (event.getGame() == Game.XZDD) {
        roomDic.load();
      }
    });
  }

  /**
   * 关机.
   * 
   * @param event 事件.
   */
  @Subscribe
  public void onShutDownRoom(ShutdownRoomEvent event) {
    gameExecutor.execute(() -> {
      if (event.getGame() == Game.XZDD) {
        roomDic.load();
        AbstractRoom room = dataMgr.getRoom(event.getRoomId());
        for (Long playerId : room.getPlayers()) {
          Player player = playerMgr.getPlayer(playerId);
          if (player != null) {
            loginMgr.noticeLogoutForGameExit(player);
          }
        }
      }
    });
  }

  @Subscribe
  public void onClearGameData(ClearGameDataEvent event) {
    gameExecutor.execute(() -> doClearGameData(event.playerId));
  }

  /**
   * 进入游戏大厅 .
   * 
   * @param player 玩家.
   */
  public void enterGameHall(Player player) {
    if (player.curRoom != null) {
      Game game = player.curRoom.game();
      LOG.warn("[血战到底]玩家[{}][{}]在[{}]游戏中，暂时无法进入游戏", player.getId(), player.getPlayerName(),
          game.getDesc());
      commonMsgMgr.sendErrorDlgMsg(player, ErrorCode.GAME_ALREADY_IN, Operator.SYSTEM,
          game.getDesc());
      return;
    }

    msgMgr.sendEnterGameHallMsg(player);
    LOG.info("[血战到底]玩家[{}][{}]进入大厅成功", player.getPlayerName(), player.getId());
  }

  /**
   * 玩家进入房间 .
   * 
   * @param player 玩家.
   * @param roomId .
   */
  public void enterRoom(Player player, int roomId) {
    AbstractRoom curRoom = player.curRoom;
    if (curRoom != null) {
      Game game = curRoom.game();
      LOG.warn("[血战到底]玩家[{}][{}]当前在游戏[{}]房间[{}]中不能进入房间", player.getId(), player.getPlayerName(),
          game, curRoom.getId());
      commonMsgMgr.sendErrorDlgMsg(player, ErrorCode.GAME_ALREADY_IN, Operator.SYSTEM,
          game.getDesc());
      return;
    }

    XzddRoom room = dataMgr.getRoom(roomId);
    if (room == null) {
      LOG.error("[血战到底]玩家[{}][{}]进入的房间[{}]不存在", player.getPlayerName(), player.getId(), roomId);
      return;
    }

    XzddRoomDomain roomDom = roomDic.get(roomId);
    if (roomDom.getIsActive() != 1) {
      LOG.error("[血战到底]玩家[{}][{}]进入的房间[{}]已经关闭", player.getPlayerName(), player.getId(), roomId);
      return;
    }

    if (player.getGold() < roomDom.getLower()) {
      commonMsgMgr.sendErrorDlgMsg(player, ErrorCode.PLAYER_GOLD_NOT_ENOUGH, Operator.SYSTEM);
      LOG.warn("[血战到底]玩家[{}][{}]进入的房间[{}]低于下限[{}]金币", player.getPlayerName(), player.getId(),
          roomId, roomDom.getLower());
      return;
    }
    if (player.getGold() > roomDom.getUpper()) {
      commonMsgMgr.sendErrorDlgMsg(player, ErrorCode.PLAYER_GOLD_BEYOND_MAX_GOLD, Operator.SYSTEM);
      LOG.warn("[血战到底]玩家[{}][{}]进入的房间[{}]高于上限[{}]金币", player.getPlayerName(), player.getId(),
          roomId, roomDom.getUpper());
      return;
    }

    if (!player.vip()
        && room.getPlayers().size() >= roomDom.getMaxNum() * roomDom.getOrdinarPeople() / 100) {
      commonMsgMgr.sendErrorDlgMsg(player, ErrorCode.GAME_NOT_VIP_ROOM_FULL, Operator.SYSTEM);
      LOG.warn("[血战到底]玩家[{}][{}]进入的房间[{}]普通玩家人数已超过上限", player.getPlayerName(), player.getId(),
          roomId, roomDom.getMaxNum());
      return;
    }

    if (room.getPlayers().size() >= roomDom.getMaxNum()) {
      commonMsgMgr.sendErrorDlgMsg(player, ErrorCode.GAME_ROOM_FULL, Operator.SYSTEM);
      LOG.warn("[血战到底]玩家[{}][{}]进入的房间[{}]已超过最大人数[{}]", player.getPlayerName(), player.getId(),
          roomId, roomDom.getMaxNum());
      return;
    }

    // 修改玩家当前房间
    player.curRoom = room;
    room.getPlayers().add(player.getId());
    // 发送进入房间消息
    msgMgr.sendEnterRoomMsg(player);
    LOG.info("[血战到底]玩家[{}][{}]进入房间[{}]成功", player.getPlayerName(), player.getId(), roomId);

    // 触发进入房间事件
    eventMgr.post(new EnterRoomEvent(player, room));
  }

  /**
   * 玩家退出房间 .
   * 
   * @param player 玩家.
   */
  public void exitRoom(Player player) {
    AbstractRoom curRoom = player.curRoom;
    XzddSeat seat = dataMgr.getPlayerSeat(player.getId());

    if (curRoom instanceof XzddRoom) {
      if (seat == null) {
        player.curRoom = null;
        XzddRoom room = (XzddRoom) curRoom;
        room.getPlayers().remove(player.getId());
        msgMgr.sendExitRoomMsg(player, 0);

        LOG.info("[血战到底]玩家[{}][{}]退出房间[{}]", player.getPlayerName(), player.getId(), room.getId());
        // 触发退出房间事件
        eventMgr.post(new ExitRoomEvent(player, curRoom));
      } else {
        LOG.error("[血战到底]玩家[{}][{}]在座位中不能退出房间,请先退出桌子", player.getPlayerName(), player.getId());
        msgMgr.sendExitRoomMsg(player, 1);
      }
    } else {
      LOG.warn("[血战到底]玩家[{}][{}]不在房间中，无法退出房间", player.getPlayerName(), player.getId());
    }
  }


  /**
   * 玩家进入牌桌 .
   * 
   * @param player 玩家.
   * @param tableId .
   * @param order .
   */
  public void enterTable(Player player, int tableId, byte order, String tablePwd) {
    if (!(player.curRoom instanceof XzddRoom)) {
      LOG.error("[血战到底]玩家[{}][{}]当前不在房间中不能进入桌子", player.getPlayerName(), player.getId());
      return;
    }

    XzddTable table = dataMgr.getTable(tableId);

    if (table == null) {
      LOG.error("[血战到底]玩家[{}][{}]进入的牌桌[{}]不存在", player.getPlayerName(), player.getId(), tableId);
      return;
    }

    if (dataMgr.getPlayerSeat(player.getId()) != null) {
      LOG.error("[血战到底]玩家[{}][{}]已经在牌桌中不能重复进入牌桌", player.getPlayerName(), player.getId());
      return;
    }

    XzddSeat seat = table.getSeat(order);
    if (seat == null) {
      LOG.error("[血战到底]玩家[{}][{}]进入的牌桌位置[{}]不合法", player.getPlayerName(), player.getId(), order);
      return;
    }

    if (seat.getPlayerId() > 0) {
      commonMsgMgr.sendErrorDlgMsg(player, ErrorCode.GAME_SET_NOT_EMPTY, Operator.SYSTEM);
      LOG.warn("[血战到底]玩家[{}][{}]进入的牌桌位置[{}]已经有人", player.getPlayerName(), player.getId(), order);
      return;
    }

    // 进入座位
    doEnterTable(player, seat, tablePwd);
  }

  /**
   * 快速进入血战到底 .
   * 
   * @param player 玩家.
   * @param roomId .
   */
  public void fastEnterTable(Player player, int roomId) {
    if (!(player.curRoom instanceof XzddRoom)) {
      LOG.error("[血战到底]玩家[{}][{}]当前不在房间中不能直接进入桌子", player.getPlayerName(), player.getId());
      return;
    }

    XzddSeat seat = dataMgr.getPlayerSeat(player.getId());
    if (seat != null) {
      LOG.error("[血战到底]玩家[{}][{}]已经在牌桌中不能重复进入牌桌", player.getPlayerName(), player.getId());
      return;
    }

    XzddRoom room = dataMgr.getRoom(roomId);
    if (room == null) {
      LOG.error("[血战到底]玩家[{}][{}]快速进入的房间[{}]不存在", player.getPlayerName(), player.getId(), roomId);
      return;
    }

    XzddSeat emptySeat = findEmptySeat(player, room);
    if (emptySeat != null) {
      doEnterTable(player, emptySeat, null);
    } else {
      // 发送快速进入桌子结果
      msgMgr.sendFastEnterTableMsg(player, 1);
      commonMsgMgr.sendTipDlgMsg(player, ErrorCode.GAME_NOT_SIT, Operator.SYSTEM);
    }
  }

  /**
   * 查找空位置,优先查找有人的桌子 .
   * 
   * @param player 玩家.
   * @param room .
   * @return
   */
  private XzddSeat findEmptySeat(Player player, XzddRoom room) {
    XzddSeat emptySeat = null;
    ArrayList<XzddTable> tables = new ArrayList<>(room.tables.values());
    Collections.shuffle(tables);

    for (XzddTable table : tables) {
      if (table.getPassword() != null) {
        continue;
      }
      if (table.stage != XzddStage.READY) {
        continue;
      }

      if (table.getLimitIp() != null && table.getLimitIp().equals(player.ip)) {
        continue;
      }

      if (player.getGold() < table.getLimitGold()) {
        continue;
      }

      boolean hasPlayer = false;
      XzddSeat tableEmptySeat = null;

      for (XzddSeat seat : table.seats) {
        if (seat.getPlayerId() == 0) {
          emptySeat = seat;
          tableEmptySeat = seat;
        } else {
          hasPlayer = true;
        }
      }

      if (hasPlayer && tableEmptySeat != null) {
        return tableEmptySeat;
      }
    }

    return emptySeat;
  }

  /**
   * 玩家进入座位 .
   * 
   * @param player 玩家.
   * @param seat 座位信息.
   */
  private void doEnterTable(Player player, XzddSeat seat, String tablePwd) {
    int roomId = seat.table.room.getId().intValue();
    XzddTable table = seat.table;

    if (table.stage != XzddStage.READY) {
      LOG.warn("[血战到底]玩家[{}][{}]进入的桌子[{}]已经开始游戏", player.getPlayerName(), player.getId(),
          table.getId());
      return;
    }

    XzddRoomDomain roomDom = roomDic.get(seat.table.room.getId().intValue());
    if (player.getGold() < roomDom.getLower()) {
      commonMsgMgr.sendErrorDlgMsg(player, ErrorCode.PLAYER_GOLD_NOT_ENOUGH, Operator.SYSTEM);
      LOG.warn("[血战到底]玩家[{}][{}]进入的房间[{}]低于下限[{}]金币", player.getPlayerName(), player.getId(),
          roomId, roomDom.getLower());
      return;
    }

    if (player.getGold() > roomDom.getUpper()) {
      commonMsgMgr.sendErrorDlgMsg(player, ErrorCode.PLAYER_GOLD_BEYOND_MAX_GOLD, Operator.SYSTEM);
      LOG.warn("[血战到底]玩家[{}][{}]进入的房间[{}]高于上限[{}]金币", player.getPlayerName(), player.getId(),
          roomId, roomDom.getUpper());
      return;
    }

    // 玩家进桌前的房间设置校验
    if (!gameHallMgr.checkTableSetting(player, seat.table(), tablePwd)) {
      return;
    }

    if (player.getGold() < roomDom.getAfee()) {
      commonMsgMgr.sendErrorDlgMsg(player, ErrorCode.GAME_NOT_ENOUGH_FEE, Operator.SYSTEM);
      LOG.warn("[血战到底]玩家[{}][{}]进入桌子金币不足台费", player.getPlayerName(), player.getId());
      return;
    } else {
      playerMgr.minusGold(player, roomDom.getAfee(), LogReason.SIREN_NIUNIU_ENTER_TABLE_MINUS_AFEE);
      DbLogService.log(new GameAfeeLog(player, table, roomDom.getAfee()));
    }

    seat.setPlayerId(player.getId());
    seat.setPlayerName(player.getPlayerName());
    seat.setSex(player.getSex());
    seat.setRobot(player.robot());
    seat.setState(SeatState.SEATED);

    // 更新玩家座位
    dataMgr.updatePlayerSeats(player.getId(), seat);
    // 触发进入桌子事件
    eventMgr.post(new EnterTableEvent(player, seat));

    // 向玩家发送进入牌桌结果信息
    msgMgr.sendEnterTableMsg(player, table);
    msgMgr.sendOtherEnterTableMsg(seat);
    LOG.info("[血战到底]玩家[{}][{}]进入桌子[{}]座位[{}]成功", player.getPlayerName(), player.getId(),
        table.getId(), seat.getOrder());

    int readyTime = timeDic.map().get(1).getTime();
    // 进入座位后如果没有在指定时间内准备好，则退出座子
    seat.stepFuture = scheduleMgr.schedule(() -> doExitTabl(seat), readyTime + ADDED_DELAY,
        TimeUnit.SECONDS, gameExecutor);
  }

  /**
   * 玩家退出牌桌 .
   * 
   * @param player 玩家.
   */
  public void exitTable(Player player) {
    XzddSeat seat = dataMgr.getPlayerSeat(player.getId());
    if (seat == null) {
      LOG.error("[血战到底]玩家[{}][{}]没在牌桌中不能退出牌桌", player.getPlayerName(), player.getId());
      return;
    }

    // 退出桌子
    doExitTabl(seat);
  }

  /**
   * 退出桌子,有人在游戏过程中强退，立刻按淘汰处理。不影响其他玩家继续游戏,强退时，扣除16*底分的金币作为强退惩罚 .
   * 
   * @param seat 座位信息.
   */
  private void doExitTabl(XzddSeat seat) {
    final XzddTable table = seat.table;
    long playerId = seat.getPlayerId();
    final Player player = playerMgr.getPlayer(playerId);

    // 退出游戏
    doExitGame(seat);
    // 发送退出房间消息
    msgMgr.sendExitTableMsg(seat);
    dataMgr.removePlayerSeat(seat.getPlayerId());
    LOG.info("[血战到底]玩家[{}][{}]退出桌子[{}]", seat.getPlayerId(), seat.getPlayerName(),
        seat.table.getId());
    seat.clear();

    if (table.players().size() == 0) {
      table.reset();
    }
    // 触发退出桌子事件
    eventMgr.post(new ExitTableEvent(player, seat));
  }

  /**
   * 退出游戏 .
   * 
   * @param seat 座位信息.
   */
  private void doExitGame(XzddSeat seat) {
    XzddTable table = seat.table;
    XzddSeatCards cards = seat.cards;
    int prePlayMj = table.prePlayMj;

    if (table.stage == XzddStage.MISSING) {
      if (table.dealer == seat) { // 定缺庄家退，解散
        resetGame(table);
        msgMgr.sendResetMsg(table);
        return;
      } else if (cards.missing == null) {
        doMissing(seat, seat.cards.minMjType());
      }
    }

    if (table.stage == XzddStage.EXCHANGE) {
      if (table.dealer == seat) { // 换牌庄家退，解散
        resetGame(table);
        msgMgr.sendResetMsg(table);
        return;
      } else if (seat.exchangeNum < 0) {
        doExchange(seat, Collections.emptyList());
      }
    }

    if (table.stage == XzddStage.GRAB && (seat.pengState == XzddGrabState.GRABING
        || seat.gangState == XzddGrabState.GRABING || seat.huState == XzddGrabState.GRABING)) {
      doPass(seat, prePlayMj);
    }

    if (table.stage == XzddStage.PLAY && table.nextPlaySeat == seat
        && cards.getTotalCount() % 3 == 2) {
      doPlay(seat, cards.autoPlayMj());
    }

    if (table.stage != XzddStage.READY) {
      if (table.notOverSeatsNum() > 1) {
        if (!seat.over) {
          seat.over = true;
          // 非准备阶段退出要扣除16倍金币
          biller.billExitGame(seat);
        }
      } else {
        seat.over = true;
        doGameOver(table);
      }
    }
  }

  /**
   * 玩家换桌 .
   * 
   * @param player 玩家.
   */
  public void exchangeTable(Player player) {
    XzddSeat seat = dataMgr.getPlayerSeat(player.getId());
    if (seat == null) {
      LOG.error("玩家[{}][{}]没在座位中不能换位置", player.getPlayerName(), player.getId());
      return;
    }

    if (seat.readied()) {
      LOG.error("[血战到底]玩家[{}][{}]已经准备好不能换桌子", player.getPlayerName(), player.getId());
      msgMgr.sendExchangeTableMsg(player, 1);
      return;
    }

    XzddRoom room = seat.table.room;

    // 退出桌子
    exitTable(player);
    // 交换到其他桌的位置
    XzddSeat exchangedSeat = findEmptySeat(player, room);
    if (exchangedSeat == null) {
      LOG.warn("[血战到底]当前房间没有空位置给玩家[{}][{}]交换", player.getPlayerName(), player.getId());
      msgMgr.sendExchangeTableMsg(player, 2);
    } else {
      // 进入位置
      doEnterTable(player, exchangedSeat, null);
    }
  }

  /**
   * 玩家准备 .
   * 
   * @param player 玩家.
   */
  public void ready(Player player) {
    // 座位
    XzddSeat seat = dataMgr.getPlayerSeat(player.getId());

    if (seat == null) {
      LOG.warn("[血战到底]玩家[{}][{}]还未进入牌桌不能准备", player.getPlayerName(), player.getId());
      return;
    }

    if (seat.readied()) {
      LOG.warn("[血战到底]玩家[{}][{}]已经准备好,请勿重复准备", player.getPlayerName(), player.getId());
      return;
    }

    XzddTable table = seat.table;
    if (table.stage != XzddStage.READY) {
      LOG.warn("[血战到底]玩家[{}][{}]当前处于非准备阶段不能准备", player.getPlayerName(), player.getId());
      return;
    }

    int roomId = table.room.getId().intValue();
    XzddRoomDomain roomDom = roomDic.get(roomId);

    if (player.getGold() < roomDom.getLower()) {
      commonMsgMgr.sendErrorDlgMsg(player, ErrorCode.PLAYER_GOLD_NOT_ENOUGH, Operator.SYSTEM);
      LOG.warn("[血战到底]玩家[{}][{}]进入的房间[{}]低于下限[{}]金币", player.getPlayerName(), player.getId(),
          roomId, roomDom.getLower());
      return;
    }

    seat.reset();
    seat.setState(SeatState.READY);
    msgMgr.sendReadyMsg(seat, 0);
    LOG.info("[血战到底]玩家[{}][{}]准备好", player.getPlayerName(), player.getId());

    // 向同桌的玩家发送该座位的玩家已经准备好
    eventMgr.post(new RoomMemInfoUpdateEvent(player));

    /*
     * 计算桌子是否准备好 .
     */
    if (table.readyOver()) { // 都准备完成,对所有玩家进行游戏过程定时(没有对每个步骤定时)
      doReadyOver(table);
    }
  }

  /**
   * 桌子全部准备完成,随机庄家，随机骰子，发牌 .
   * 
   * @param table .
   */
  private void doReadyOver(XzddTable table) {
    // 定缺时间
    int missingTime = timeDic.get(2).getTime();
    List<XzddSeat> seats = table.seats;
    Map<Long, XzddPlayerBill> playerBills = table.playerBills;

    for (int i = 0; i < seats.size(); i++) {
      XzddSeat s = seats.get(i);
      playerBills.put(s.getPlayerId(), new XzddPlayerBill(s.getOrder(), s.getPlayerName(), -1, 0));
      s.setState(SeatState.GAMING);
      // 清除准备计时
      s.stepFuture.cancel(false);
      // 触发房间大厅成员信息变更事件(玩家状态变为游戏中需要广播出去)
      eventMgr.post(new RoomMemInfoUpdateEvent(playerMgr.getPlayer(s.getPlayerId())));
      s.stepFuture = scheduleMgr.schedule(() -> doMissing(s, s.cards.minMjType()),
          missingTime + ADDED_DELAY, TimeUnit.SECONDS, gameExecutor);
    }

    table.stage = XzddStage.MISSING;
    table.dealer = RandCodeUtil.randomList(table.seats);
    table.dice1 = RandCodeUtil.random(1, 6);
    table.dice2 = RandCodeUtil.random(1, 6);
    table.nextPlaySeat = table.dealer;
    doDealCards(table);
    msgMgr.sendReadyOverMsg(table);
  }

  /**
   * 发牌 .
   * 
   * @param table .
   */
  /*
   * private . void doDealCards(XzddTable table) { List<XzddMj> mjs = table.mjs; // 打乱牌
   * Collections.shuffle(mjs);
   * 
   * // . 摸牌index int drawIndex = 0; for (; drawIndex < PLAYER_CARDS * 4; drawIndex++) { XzddMj mj =
   * mjs.get(drawIndex); table.seats.get(drawIndex % 4).cards.addMj(mj); }
   * 
   * // . 计算胡牌 for (XzddSeat seat : table.seats) { seat.cards.calcHuMjs(); }
   * 
   * // . 庄家多一张 table.dealer.cards.addMj(mjs.get(drawIndex++)); table.dealer.drawCount++;
   * table.drawIndex . = drawIndex; }
   */
  private void doDealCards(XzddTable table) {
    List<Integer> testMjs = new ArrayList<>();
    for (int i = 1; i <= XzddSeatCards.MJ_MAX_ID; i++) {
      testMjs.add(i);
      testMjs.add(i);
      testMjs.add(i);
      testMjs.add(i);
    }
    // 打乱牌
    Collections.shuffle(testMjs);

    final XzddSeat seat1 = table.getSeat(0);
    final XzddSeat seat2 = table.getSeat(1);
    final XzddSeat seat3 = table.getSeat(2);
    final XzddSeat seat4 = table.getSeat(3);

    seat1.cards.addMj(XzddMj.YI_WAN);
    seat1.cards.addMj(XzddMj.YI_WAN);
    seat1.cards.addMj(XzddMj.YI_WAN);
    seat1.cards.addMj(XzddMj.ER_WAN);
    seat1.cards.addMj(XzddMj.SAN_WAN);
    seat1.cards.addMj(XzddMj.SI_WAN);
    seat1.cards.addMj(XzddMj.WU_WAN);
    seat1.cards.addMj(XzddMj.LIU_WAN);
    seat1.cards.addMj(XzddMj.QI_WAN);
    seat1.cards.addMj(XzddMj.BA_WAN);
    seat1.cards.addMj(XzddMj.JIU_WAN);
    seat1.cards.addMj(XzddMj.JIU_WAN);
    seat1.cards.addMj(XzddMj.JIU_WAN);

    seat2.cards.addMj(XzddMj.YI_TONG);
    seat2.cards.addMj(XzddMj.YI_TONG);
    seat2.cards.addMj(XzddMj.YI_TONG);
    seat2.cards.addMj(XzddMj.ER_TONG);
    seat2.cards.addMj(XzddMj.ER_TONG);
    seat2.cards.addMj(XzddMj.ER_TONG);
    seat2.cards.addMj(XzddMj.SAN_TONG);
    seat2.cards.addMj(XzddMj.SAN_TONG);
    seat2.cards.addMj(XzddMj.SAN_TONG);
    seat2.cards.addMj(XzddMj.SI_TONG);
    seat2.cards.addMj(XzddMj.SI_TONG);
    seat2.cards.addMj(XzddMj.WU_TONG);
    seat2.cards.addMj(XzddMj.WU_TONG);

    seat3.cards.addMj(XzddMj.YI_TIAO);
    seat3.cards.addMj(XzddMj.YI_TIAO);
    seat3.cards.addMj(XzddMj.YI_TIAO);
    seat3.cards.addMj(XzddMj.ER_TIAO);
    seat3.cards.addMj(XzddMj.ER_TIAO);
    seat3.cards.addMj(XzddMj.ER_TIAO);
    seat3.cards.addMj(XzddMj.SAN_TIAO);
    seat3.cards.addMj(XzddMj.SAN_TIAO);
    seat3.cards.addMj(XzddMj.SAN_TIAO);
    seat3.cards.addMj(XzddMj.JIU_TIAO);
    seat3.cards.addMj(XzddMj.JIU_TIAO);
    seat3.cards.addMj(XzddMj.JIU_TONG);
    seat3.cards.addMj(XzddMj.JIU_TONG);

    seat4.cards.addMj(XzddMj.ER_WAN);
    seat4.cards.addMj(XzddMj.ER_WAN);
    seat4.cards.addMj(XzddMj.SAN_WAN);
    seat4.cards.addMj(XzddMj.SAN_WAN);
    seat4.cards.addMj(XzddMj.SI_WAN);
    seat4.cards.addMj(XzddMj.SI_WAN);
    seat4.cards.addMj(XzddMj.WU_WAN);
    seat4.cards.addMj(XzddMj.WU_WAN);
    seat4.cards.addMj(XzddMj.LIU_WAN);
    seat4.cards.addMj(XzddMj.LIU_WAN);
    seat4.cards.addMj(XzddMj.QI_WAN);
    seat4.cards.addMj(XzddMj.QI_WAN);
    seat4.cards.addMj(XzddMj.BA_WAN);

    for (XzddSeat seat : table.seats) {
      for (Integer mj : seat.cards.mjList()) {
        testMjs.remove(mj);
      }
    }

    for (XzddSeat seat : table.seats) {
      for (Integer mj : seat.cards.mjList()) {
        testMjs.add(0, mj);
      }
      // 计算胡牌
      seat.cards.calcHuMjs();
    }

    List<XzddMj> tableMjs = table.mjs;
    tableMjs.clear();
    for (int testMjId : testMjs) {
      tableMjs.add(XzddMj.getMj(testMjId));
    }

    // 摸牌index
    int drawIndex = 52;
    // 庄家多一张
    XzddMj dealerMj = tableMjs.get(drawIndex++);
    table.dealer.cards.addMj(dealerMj);
    table.dealer.drawCount++;
    table.drawIndex = drawIndex;
  }

  /**
   * 定缺 .
   * 
   * @param player 玩家.
   * @param missing . 小于0:万,0:筒 ,大于0:条
   */
  public void missing(Player player, int missing) {
    // 座位
    XzddSeat seat = dataMgr.getPlayerSeat(player.getId());

    if (seat == null) {
      LOG.error("[血战到底]玩家[{}][{}]还未进入牌桌不能定缺", player.getPlayerName(), player.getId());
      return;
    }

    if (seat.cards.missing != null) {
      LOG.warn("[血战到底]玩家[{}][{}]已经定缺,请勿重复定缺", player.getPlayerName(), player.getId());
      return;
    }

    XzddTable table = seat.table;
    if (table.stage != XzddStage.MISSING) {
      LOG.warn("[血战到底]玩家[{}][{}]当前处于非定缺阶段不能定缺", player.getPlayerName(), player.getId());
      return;
    }

    // 缺的类型
    XzddMjType missingType = null;
    if (missing < 0) {
      missingType = XzddMjType.WAN;
    } else if (missing > 0) {
      missingType = XzddMjType.TIAO;
    } else {
      missingType = XzddMjType.TONG;
    }

    // 定缺
    doMissing(seat, missingType);
  }

  /**
   * 定缺 .
   * 
   * @param seat 座位信息.
   */
  private void doMissing(XzddSeat seat, XzddMjType missingType) {
    seat.cards.missing = missingType;
    // 清除定缺计时
    seat.stepFuture.cancel(false);

    msgMgr.sendMissingMsg(seat);
    LOG.info("[血战到底]玩家[{}][{}]定缺[{}]", seat.getPlayerName(), seat.getPlayerId(), missingType);

    /*
     * 计算桌子是否定缺结束 .
     */
    XzddTable table = seat.table;
    if (table.missingOver()) {
      doMissingOver(table);
    }
  }

  /**
   * 定缺完成 .
   * 
   * @param table .
   */
  private void doMissingOver(XzddTable table) {
    // 换牌时间
    int exchangeMjTime = timeDic.get(3).getTime();
    for (XzddSeat s : table.seats) {
      if (s.getPlayerId() > 0 && !s.over) {
        // 清除定缺计时
        s.stepFuture.cancel(false);
        // 换牌计时
        s.stepFuture = scheduleMgr.schedule(() -> doExchange(s, Collections.emptyList()),
            exchangeMjTime + ADDED_DELAY, TimeUnit.SECONDS, gameExecutor);
      }
    }

    table.stage = XzddStage.EXCHANGE;
    msgMgr.sendMissingOverMsg(table);
  }

  /**
   * 换牌 .
   * 
   * @param player 玩家.
   * @param mjIds .
   */
  public void exchange(Player player, List<Integer> mjIds) {
    // 座位
    XzddSeat seat = dataMgr.getPlayerSeat(player.getId());

    if (seat == null) {
      LOG.error("[血战到底]玩家[{}][{}]还未进入牌桌不能换牌", player.getPlayerName(), player.getId());
      return;
    }

    if (seat.exchangeNum >= 0) {
      LOG.warn("[血战到底]玩家[{}][{}]已经换过牌,请勿重复换牌", player.getPlayerName(), player.getId());
      return;
    }

    if (mjIds.size() > EXCHANGE_MAX) {
      LOG.warn("[血战到底]玩家[{}][{}]换牌数量[{}]超过[{}]", player.getPlayerName(), player.getId(),
          mjIds.size(), EXCHANGE_MAX);
      return;
    }

    XzddTable table = seat.table;
    if (table.stage != XzddStage.EXCHANGE) {
      LOG.warn("[血战到底]玩家[{}][{}]当前处于非换牌阶段不能换牌", player.getPlayerName(), player.getId());
      return;
    }

    if (!seat.cards.contains(mjIds)) {
      LOG.warn("[血战到底]玩家[{}][{}]不包含牌[{}]不能换牌", player.getPlayerName(), player.getId(), mjIds);
      return;
    }

    // 换牌
    doExchange(seat, mjIds);
  }

  /**
   * 换牌 .
   * 
   * @param seat 座位信息.
   * @param mjIds .
   */
  private void doExchange(XzddSeat seat, List<Integer> mjIds) {
    final XzddTable table = seat.table;
    // 清除换牌计时
    seat.stepFuture.cancel(false);
    seat.exchangeNum = mjIds.size();
    seat.cards.removeMjs(mjIds);
    table.exchangeMjs.addAll(mjIds);

    msgMgr.sendExchangeMsg(seat);
    LOG.info("[血战到底]玩家[{}][{}]换牌[{}]", seat.getPlayerName(), seat.getPlayerId(), mjIds);

    /*
     * 计算桌子是否换牌结束 .
     */
    if (table.exchangeOver()) {
      doExchangeOver(table);
    }
  }

  /**
   * 换牌结束 .
   * 
   * @param table .
   */
  private void doExchangeOver(XzddTable table) {
    List<XzddSeat> seats = table.seats;
    List<Integer> exchangeMjs = table.exchangeMjs;
    Collections.shuffle(exchangeMjs);
    int exchangeIndex = 0;
    XzddSeat dealer = table.dealer;

    for (int i = 0; i < seats.size(); i++) {
      XzddSeat seat = seats.get(i);
      if (seat.getPlayerId() > 0 && !seat.over) {
        List<Integer> seatExchangeMjs =
            exchangeMjs.subList(exchangeIndex, exchangeIndex += seat.exchangeNum);
        // 清除换牌计时
        seat.stepFuture.cancel(false);
        seat.cards.addMjs(seatExchangeMjs);
        seat.exchangeMjs = seatExchangeMjs;
        msgMgr.sendExchangeOverMsg(seat);

        /*
         * 闲家胡牌提示 .
         */
        if (seat != dealer) {
          seat.cards.calcHuMjs();
          // 有没有胡牌都发送胡牌提示消息，客户端根据数据提示
          msgMgr.sendHuPaiMsg(seat);
        }
      }
    }

    /*
     * 判断庄家是否可以胡和杠以及听牌 .
     */
    XzddSeatCards dealerCards = dealer.cards;
    // 是否有杠、胡抢牌操作
    boolean gangAble = false;
    for (int mj = 1; mj <= XzddSeatCards.MJ_MAX_ID; mj++) {
      if (dealerCards.contains(mj) && dealerCards.gangAble(mj)) {
        gangAble = true;
        break;
      }
    }
    boolean huAble = XzddCardsAnalyzer.huPaiType(dealerCards) != null;
    dealer.pengState = null;
    dealer.gangState = gangAble ? XzddGrabState.GRABING : null;
    dealer.huState = huAble ? XzddGrabState.GRABING : null;

    // 听牌信息
    Multimap<Integer, Integer> tingMjs = null;
    if (gangAble || huAble) { // 是否可以胡和杠
      table.stage = XzddStage.GRAB;
      dealer.stepFuture = scheduleMgr.schedule(() -> doPass(dealer, dealer.cards.getLastDrawMj()),
          timeDic.get(5).getTime() + ADDED_DELAY, TimeUnit.SECONDS, gameExecutor);
      msgMgr.sendGrabAbleMsg(dealer, false, gangAble, huAble);
      return;
    } else if ((tingMjs = dealer.cards.calcTingMjs()).size() > 0) { // 可以听牌
      msgMgr.sendTingPaiMsg(dealer, tingMjs);
    }

    /*
     * 庄家schedule出牌 .
     */
    // 出牌时间
    int playTime = timeDic.get(4).getTime();
    // 出牌计时
    dealer.stepFuture = scheduleMgr.schedule(() -> doPlay(dealer, dealer.cards.autoPlayMj()),
        playTime + ADDED_DELAY, TimeUnit.SECONDS, gameExecutor);
    table.stage = XzddStage.PLAY;
  }

  /**
   * 出牌 .
   * 
   * @param player 玩家.
   * @param mj .
   */
  public void play(Player player, int mj) {
    // 座位
    XzddSeat seat = dataMgr.getPlayerSeat(player.getId());
    if (seat == null) {
      LOG.error("[血战到底]玩家[{}][{}]还未进入牌桌不能出牌", player.getPlayerName(), player.getId());
      return;
    }

    XzddTable table = seat.table;
    if (seat != table.nextPlaySeat) {
      LOG.warn("[血战到底]当前出牌玩家顺序[{}]不该玩家[{}][{}]出牌", table.nextPlaySeat, player.getPlayerName(),
          player.getId());
      return;
    }

    if (table.stage != XzddStage.PLAY) {
      LOG.warn("[血战到底]玩家[{}][{}]当前处于非出牌阶段不能出牌", player.getPlayerName(), player.getId());
      return;
    }

    if (!seat.cards.contains(mj)) {
      LOG.warn("[血战到底]玩家[{}][{}]不包含牌[{}]不能出牌", player.getPlayerName(), player.getId(), mj);
      return;
    }

    if (seat.cards.getMissingCount() > 0 && !seat.cards.missing(mj)) {
      LOG.warn("[血战到底]玩家[{}][{}]缺牌还未出完不能出牌", player.getPlayerName(), player.getId(), mj);
      return;
    }

    // 出牌
    doPlay(seat, mj);
  }

  /**
   * 出牌 .
   * 
   * @param seat 座位信息.
   * @param mj .
   */
  private void doPlay(XzddSeat seat, int mj) {
    XzddTable table = seat.table;
    XzddSeatCards cards = seat.cards;

    seat.stepFuture.cancel(false);
    cards.playMj(mj);
    table.prePlayMj = mj;
    table.prePlayer = seat.getPlayerId();
    table.nextPlaySeat = table.nextPlaySeat(seat);

    // 发送出牌消息
    msgMgr.sendPlayMsg(seat, mj);
    LOG.info("[血战到底]玩家[{}][{}]出牌[{}]", seat.getPlayerName(), seat.getPlayerId(), mj);

    // 有没有胡牌都发送胡牌提示消息，客户端根据数据提示
    msgMgr.sendHuPaiMsg(seat);

    // 抢牌时间
    int grabTime = timeDic.get(5).getTime();
    for (XzddSeat s : table.seats) {
      if (s != seat && s.getPlayerId() > 0 && !s.over) {
        // 是否有碰、杠、胡抢牌操作
        boolean pengAble = s.cards.pengAble(mj);
        boolean gangAble = s.cards.gangAble(mj);
        boolean huAble = s.cards.huAble(mj, false);
        s.pengState = pengAble ? XzddGrabState.GRABING : null;
        s.gangState = gangAble ? XzddGrabState.GRABING : null;
        s.huState = huAble ? XzddGrabState.GRABING : null;

        if (pengAble || gangAble || huAble) { // 是否可以抢牌(有碰杠胡)
          table.stage = XzddStage.GRAB;
          s.stepFuture.cancel(false);
          s.stepFuture = scheduleMgr.schedule(() -> doPass(s, mj), grabTime + ADDED_DELAY,
              TimeUnit.SECONDS, gameExecutor);
          msgMgr.sendGrabAbleMsg(s, pengAble, gangAble, huAble);
        }
      }
    }

    if (table.stage == XzddStage.GRAB) {
      return;
    }

    if (table.gameOver()) { // 游戏结束
      doGameOver(table);
    } else { // 游戏没有结束且没人可以抢牌，则下一个出牌玩家摸牌
      table.stage = XzddStage.PLAY;
      doDraw(table.nextPlaySeat);
    }
  }

  /**
   * 摸牌 .
   * 
   * @param seat 座位信息.
   */
  private void doDraw(XzddSeat seat) {
    XzddTable table = seat.table;
    XzddSeatCards cards = seat.cards;
    if (table.gameOver()) {
      doGameOver(table);
      return;
    }
    // 摸牌麻将
    final XzddMj drawMj = table.mjs.get(table.drawIndex++);

    // 放了后除非高于已放炮翻数否则不能胡,摸牌后至为0
    cards.passHuMaxMultiple = -1;
    seat.drawCount++;
    seat.stepFuture.cancel(false);
    cards.drawMj(drawMj.id);
    // 向摸牌玩家发送摸牌消息
    msgMgr.sendDrawMsg(seat, drawMj.id);
    // 发送玩家摸过牌,除了摸牌玩家
    msgMgr.sendDrewMsg(seat);
    LOG.info("[血战到底]玩家[{}][{}]摸牌[{}]", seat.getPlayerName(), seat.getPlayerId(), drawMj);

    // 听牌信息
    Multimap<Integer, Integer> tingMjs = null;
    // 是否有杠、胡抢牌操作
    boolean gangAble = false;
    boolean huAble = seat.cards.huAble(drawMj.id, true);
    for (int mj = 1; mj <= XzddSeatCards.MJ_MAX_ID; mj++) {
      if (cards.contains(mj) && cards.gangAble(mj)) {
        gangAble = true;
        break;
      }
    }

    if (gangAble || huAble) {
      table.stage = XzddStage.GRAB;
      seat.pengState = null;
      seat.gangState = gangAble ? XzddGrabState.GRABING : null;
      seat.huState = huAble ? XzddGrabState.GRABING : null;
      seat.stepFuture = scheduleMgr.schedule(() -> doPass(seat, drawMj.id),
          timeDic.get(5).getTime() + ADDED_DELAY, TimeUnit.SECONDS, gameExecutor);
      msgMgr.sendGrabAbleMsg(seat, false, gangAble, huAble);
    } /*
       * else . if ((tingMjs = cards.calcTingMjs()).size() > 0) {// 可以听牌 msgMgr.sendTingPaiMsg(seat,
       * tingMjs); }
       */

    if (!huAble && (tingMjs = cards.calcTingMjs()).size() > 0) { // 可以听牌
      msgMgr.sendTingPaiMsg(seat, tingMjs);
    }

    // 出牌时间
    int playTime = timeDic.get(4).getTime();
    // 自动出牌麻将
    int autoPlayMj = cards.autoPlayMj();
    seat.stepFuture = scheduleMgr.schedule(() -> doPlay(seat, autoPlayMj), playTime + ADDED_DELAY,
        TimeUnit.SECONDS, gameExecutor);
  }


  /**
   * 碰 .
   * 
   * @param player 玩家.
   */
  public void peng(Player player) {
    // 座位
    XzddSeat seat = dataMgr.getPlayerSeat(player.getId());
    if (seat == null) {
      LOG.error("[血战到底]玩家[{}][{}]还未进入牌桌不能抢牌", player.getPlayerName(), player.getId());
      return;
    }

    XzddTable table = seat.table;
    // 抢牌阶段和自己摸牌进入出牌阶段可以抢牌
    if (table.stage != XzddStage.GRAB) {
      LOG.warn("[血战到底]玩家[{}][{}]当前处于[{}]阶段不能碰", player.getPlayerName(), player.getId(),
          table.stage);
      return;
    }

    XzddSeatCards cards = seat.cards;
    // 碰麻将
    int pengMj = table.prePlayMj;

    if (!cards.pengAble(pengMj)) {
      LOG.warn("[血战到底]玩家[{}][{}]没有满足碰牌条件不能碰", player.getPlayerName(), player.getId());
      return;
    }

    doPeng(seat, pengMj);
  }

  /**
   * 杠 .
   * 
   * @param player 玩家.
   */
  public void gang(Player player, int mj) {
    // 座位
    XzddSeat seat = dataMgr.getPlayerSeat(player.getId());
    if (seat == null) {
      LOG.error("[血战到底]玩家[{}][{}]还未进入牌桌不能抢牌", player.getPlayerName(), player.getId());
      return;
    }

    XzddTable table = seat.table;
    // 抢牌阶段和自己摸牌进入出牌阶段可以抢牌
    if (table.stage != XzddStage.GRAB) {
      LOG.warn("[血战到底]玩家[{}][{}]当前处于[{}]阶段不能杠", player.getPlayerName(), player.getId(),
          table.stage);
      return;
    }

    XzddSeatCards cards = seat.cards;
    // 杠牌的麻将(玩家手上有多个杠时需要选择,为0时由服务器计算)
    int gangMj = 0;

    if (mj == 0) {
      if (cards.getTotalCount() % 3 == 2) { // 玩家已摸牌
        List<Integer> gangAbleMjs = new ArrayList<>();
        for (int mjTmp = 1; mjTmp <= XzddSeatCards.MJ_MAX_ID; mjTmp++) {
          if (cards.contains(mjTmp) && cards.gangAble(mjTmp)) {
            gangAbleMjs.add(mjTmp);
          }
        }

        if (gangAbleMjs.size() != 1) {
          LOG.warn("[血战到底]玩家[{}][{}]摸牌后可以杠的牌不为1不能杠", player.getPlayerName(), player.getId());
          return;
        }

        gangMj = gangAbleMjs.get(0);
      } else { // 玩家未摸牌
        gangMj = table.prePlayMj;
      }
    } else if (cards.gangAble(mj)) {
      gangMj = mj;
    } else {
      LOG.warn("[血战到底]玩家[{}][{}]不能杠牌[{}]", player.getPlayerName(), player.getId(), mj);
      return;
    }

    XzddGangType gangType = cards.gangType(gangMj);
    if (gangType == null) {
      LOG.warn("[血战到底]玩家[{}][{}]不能杠牌[{}]", player.getPlayerName(), player.getId(), mj);
      return;
    }

    doGang(seat, gangMj, cards.gangType(gangMj));
  }


  /**
   * 抢牌 .
   * 
   * @param player 玩家.
   */
  public void hu(Player player) {
    // 座位
    XzddSeat seat = dataMgr.getPlayerSeat(player.getId());
    if (seat == null) {
      LOG.error("[血战到底]玩家[{}][{}]还未进入牌桌不能抢牌", player.getPlayerName(), player.getId());
      return;
    }

    XzddTable table = seat.table;
    // 抢牌阶段和自己摸牌进入出牌阶段可以抢牌
    if (table.stage != XzddStage.GRAB) {
      LOG.warn("[血战到底]玩家[{}][{}]当前处于[{}]阶段不能胡牌", player.getPlayerName(), player.getId(),
          table.stage);
      return;
    }

    XzddSeatCards cards = seat.cards;
    // 胡牌麻将,先是否自摸，是否抢杠胡(抢杠胡的胡牌是杠牌)，是否点炮
    int huMj = table.nextPlaySeat == seat && cards.getTotalCount() % 3 == 2 ? cards.getLastDrawMj()
        : table.preMianXiaGangMj == 0 ? table.prePlayMj : table.preMianXiaGangMj;

    if (table.nextPlaySeat == seat && cards.getTotalCount() % 3 == 2) { // 自摸?
      huMj = cards.getLastDrawMj();
      if (!cards.huAble(huMj, true)) {
        LOG.warn("[血战到底]玩家[{}][{}]自摸没有满足胡牌条件不能胡", player.getPlayerName(), player.getId());
        return;
      }
    } else if (table.preMianXiaGangMj != 0) { // 抢杠胡?
      huMj = table.preMianXiaGangMj;
      if (!cards.huAble(huMj, false)) {
        LOG.warn("[血战到底]玩家[{}][{}]抢杠胡没有满足胡牌条件不能胡", player.getPlayerName(), player.getId());
        return;
      }
    } else { // 点炮?
      huMj = table.prePlayMj;
      if (!cards.huAble(huMj, false)) {
        LOG.warn("[血战到底]玩家[{}][{}]点炮没有满足胡牌条件不能胡", player.getPlayerName(), player.getId());
        return;
      }
    }

    doHu(seat, huMj);
  }

  /**
   * 过 .
   * 
   * @param player 玩家.
   */
  public void pass(Player player) {
    // 座位
    XzddSeat seat = dataMgr.getPlayerSeat(player.getId());
    if (seat == null) {
      LOG.error("[血战到底]玩家[{}][{}]还未进入牌桌不能抢牌", player.getPlayerName(), player.getId());
      return;
    }

    XzddTable table = seat.table;
    // 抢牌阶段和自己摸牌进入出牌阶段可以抢牌
    if (table.stage != XzddStage.GRAB) {
      LOG.warn("[血战到底]玩家[{}][{}]当前处于[{}]阶段不能过", player.getPlayerName(), player.getId(),
          table.stage);
      return;
    }

    XzddSeatCards cards = seat.cards;
    // 过的麻将
    int passMj =
        table.nextPlaySeat == seat && cards.getTotalCount() % 3 == 2 ? cards.getLastDrawMj()
            : table.prePlayMj;
    doPass(seat, passMj);
  }

  /**
   * 碰 .
   * 
   * @param seat 座位信息.
   * @param mj .
   */
  private void doPeng(XzddSeat seat, int mj) {
    final XzddTable table = seat.table;
    boolean huAble = seat.huState == XzddGrabState.GRABING;
    XzddSeatCards cards = seat.cards;
    seat.stepFuture.cancel(false);
    seat.pengState = XzddGrabState.GRABED;
    seat.gangState = null;
    seat.huState = null;

    LOG.info("[血战到底]玩家[{}][{}]请求碰牌[{}]", seat.getPlayerName(), seat.getPlayerId(), mj);

    /*
     * 碰杠胡是同时进行的，胡的优先级比碰和明杠高,胡过或正在胡不能碰和明杠 .
     */
    if (!table.hued(seat) && !table.huIng(seat)) {
      cards.peng(mj);
      LOG.info("[血战到底]玩家[{}][{}]碰牌[{}]", seat.getPlayerName(), seat.getPlayerId(), mj);
      // 听牌信息
      Multimap<Integer, Integer> calcTingMjs = null;
      // 可以听牌
      if (!huAble && (calcTingMjs = cards.calcTingMjs()).size() > 0) {
        msgMgr.sendTingPaiMsg(seat, calcTingMjs);
      }
      table.nextPlaySeat = seat;
      table.stage = XzddStage.PLAY;
      msgMgr.sendGrabedMsg(seat, XzddGrabType.PENG, null, mj);
      table.resetSeatsGrabState();
      // 出牌时间
      int playTime = timeDic.get(4).getTime();
      // 出牌计时
      seat.stepFuture = scheduleMgr.schedule(() -> doPlay(seat, seat.cards.autoPlayMj()),
          playTime + ADDED_DELAY, TimeUnit.SECONDS, gameExecutor);
    } else if (table.grabOver()) {
      // 比如 一家出2万，其余两家一个可以胡，一个可以胡和碰，胡的那家胡了，剩余这家点击碰（不能碰,等于做了pass操作）
      table.stage = XzddStage.PLAY;
      msgMgr.sendGrabedMsg(seat, XzddGrabType.PASS, null, mj);
      doDraw(table.nextPlaySeat);
    }
  }

  /**
   * 杠 .
   * 
   * @param seat 座位信息.
   * @param mj .
   * @param gangType .
   */
  private void doGang(XzddSeat seat, int mj, XzddGangType gangType) {
    XzddTable table = seat.table;
    seat.gangState = XzddGrabState.GRABED;
    seat.pengState = null;
    seat.huState = null;
    seat.stepFuture.cancel(false);
    seat.cards.gangIngMj = mj;
    LOG.info("[血战到底]玩家[{}][{}]请求杠牌[{}]", seat.getPlayerName(), seat.getPlayerId(), mj);

    /*
     * 面下杠其他玩家可以抢杠胡 .
     */
    if (gangType == XzddGangType.MIAN_XIA_GANG) {
      boolean qiangGangHu = false;
      // 抢牌时间
      int grabTime = timeDic.get(5).getTime();
      for (XzddSeat s : table.seats) {
        if (s != seat && s.getPlayerId() > 0 && !s.over) {
          // 面下杠其他玩家可以抢杠胡
          boolean huAble = s.cards.huAble(mj, false);

          if (huAble && s.huState == null) { // 是否可以抢牌(有碰杠胡)
            qiangGangHu = true;
            s.huState = XzddGrabState.GRABING;
            table.stage = XzddStage.GRAB;
            s.stepFuture.cancel(false);
            s.stepFuture = scheduleMgr.schedule(() -> doPass(s, mj), grabTime + ADDED_DELAY,
                TimeUnit.SECONDS, gameExecutor);
            msgMgr.sendGrabAbleMsg(s, false, false, huAble);
          }
        }
      }

      if (qiangGangHu) { // 可以抢杠胡
        table.preMianXiaGangMj = mj;
        table.preMianXiaGangPlayer = seat.getPlayerId();
        return;
      }
    }

    /*
     * 碰杠胡是同时进行的，胡的优先级比碰和明杠高,胡过或正在胡不能碰和明杠,暗杠可以直接杠 .
     */
    if (gangType == XzddGangType.AN_GANG || (!table.hued(seat) && !table.huIng(seat))) {
      seat.cards.gang(mj, gangType);
      LOG.info("[血战到底]玩家[{}][{}]杠牌[{}]", seat.getPlayerName(), seat.getPlayerId(), mj);
      table.nextPlaySeat = seat;
      table.stage = XzddStage.PLAY;
      table.resetSeatsGrabState();
      msgMgr.sendGrabedMsg(seat, XzddGrabType.GANG, gangType, mj);
      // 刮风下雨结算项(杠)
      biller.billGang(seat, gangType);
      doDraw(seat);
    } else if (table.grabOver()) {
      // 比如 一家出2万，其余两家一个可以胡，一个可以胡和杠，胡的那家胡了，剩余这家点击杠（不能杠,等于做了pass操作）
      table.stage = XzddStage.PLAY;
      doDraw(table.nextPlaySeat);
      msgMgr.sendGrabedMsg(seat, XzddGrabType.PASS, null, mj);
    }
  }

  /**
   * 胡 .
   * 
   * @param seat 座位信息.
   * @param mj .
   */
  private void doHu(XzddSeat seat, int mj) {
    XzddTable table = seat.table;
    XzddSeatCards cards = seat.cards;
    final int baseGold = roomDic.get(table.room.getId().intValue()).getMinOne();
    // 是否自摸
    boolean isZiMo = table.nextPlaySeat == seat && cards.getTotalCount() % 3 == 2;
    // 胡的类型
    XzddHuType huType = XzddCardsAnalyzer.huType(cards, mj, isZiMo);

    table.nextPlaySeat = table.nextPlaySeat(seat);
    seat.over = true;
    seat.huState = XzddGrabState.GRABED;
    seat.pengState = null;
    seat.gangState = null;
    seat.stepFuture.cancel(false);
    msgMgr.sendGrabedMsg(seat, XzddGrabType.HU, null, mj);

    if (!isZiMo) {
      cards.addMj(mj);
      if (huType == XzddHuType.QIANG_GANG_HU) {
        XzddSeat gangedSeat = table.gangedSeat();
        // 移除被抢杠胡的牌
        gangedSeat.cards.removeMj(mj);
      }
    }

    XzddHuPaiType huPaiType = XzddCardsAnalyzer.huPaiType(cards);
    // 胡牌结果明细数据
    Pair<Integer, List<HuPaiItemInfo>> huPaiData =
        XzddCalculator.calcHuData(cards, huType, huPaiType);
    // 胡牌翻数，未乘人数
    int huMultiple = huPaiData.v1;
    // 胡牌金额，未乘人数
    int huGold = baseGold << huMultiple;
    // 结算胡,返回被胡的人数
    int loserNum = biller.billHu(seat, huPaiData.v1, huGold, isZiMo, huType);
    // 发送胡消息
    msgMgr.sendHuMsg(seat, isZiMo, huType, mj, huPaiData.v2, huMultiple, huGold * loserNum);

    LOG.info("[血战到底]玩家[{}][{}]胡牌[{}]", seat.getPlayerName(), seat.getPlayerId(), mj);

    // 有人胡则其他人的碰、杠pass
    for (XzddSeat s : table.seats) {
      if (s != seat && s.getPlayerId() > 0 && s.huState != XzddGrabState.GRABING
          && (s.pengState == XzddGrabState.GRABING || s.gangState == XzddGrabState.GRABING)) {
        doPass(s, mj);
      }
    }

    if (table.gameOver()) {
      doGameOver(table);
    } else if (!table.huIng(seat)) {
      table.resetSeatsGrabState();
      if (table.nextPlaySeat.cards.getTotalCount() % 3 != 2) { // doPass也会摸牌
        table.stage = XzddStage.PLAY;
        doDraw(table.nextPlaySeat);
      }
    }
  }

  /**
   * 过,不碰杠胡 .
   * 
   * @param seat 座位信息.
   */
  private void doPass(XzddSeat seat, int mj) {
    final XzddTable table = seat.table;
    final XzddSeatCards cards = seat.cards;
    seat.stepFuture.cancel(false);
    seat.pengState = XzddGrabState.PASS;
    seat.gangState = XzddGrabState.PASS;
    seat.huState = XzddGrabState.PASS;
    msgMgr.sendGrabedMsg(seat, XzddGrabType.PASS, null, mj);

    LOG.info("[血战到底]玩家[{}][{}]过", seat.getPlayerName(), seat.getPlayerId());

    if (cards.getTotalCount() % 3 != 2 && cards.huAble(mj, false)) { // 放炮?
      seat.cards.passHuMaxMultiple = cards.getHuMjs().get(mj);
    }

    /*
     * 抢牌结束后处理,碰杠胡是同时进行的,抢牌结束后,如果没人胡牌则看是否有人碰过或杠过，有则碰和杠 .
     */
    if (table.grabOver()) {
      if (!table.hued(null)) {
        for (XzddSeat s : table.seats) {
          if (s.pengState == XzddGrabState.GRABED) {
            doPeng(s, table.prePlayMj);
            break;
          } else if (s.gangState == XzddGrabState.GRABED) {
            doGang(s, s.cards.gangIngMj, s.cards.gangType(s.cards.gangIngMj));
            break;
          }
        }
      }

      table.stage = XzddStage.PLAY;
      table.resetSeatsGrabState();

      XzddSeat nextPlaySeat = table.nextPlaySeat;

      if (nextPlaySeat.getPlayerId() > 0 && nextPlaySeat.cards.getTotalCount() % 3 == 2) {
        // eg:自己可以杠或胡的是pass要定时出牌
        nextPlaySeat.stepFuture.cancel(false);
        // 出牌时间
        int playTime = timeDic.get(4).getTime();
        // 自动出牌麻将
        int autoPlayMj = nextPlaySeat.cards.autoPlayMj();
        nextPlaySeat.stepFuture = scheduleMgr.schedule(() -> doPlay(nextPlaySeat, autoPlayMj),
            playTime + ADDED_DELAY, TimeUnit.SECONDS, gameExecutor);
      }

      if (table.hasMj()) {
        if (nextPlaySeat.getPlayerId() > 0 && nextPlaySeat.cards.getTotalCount() % 3 != 2) {
          doDraw(nextPlaySeat);
        }
      } else {
        doGameOver(table);
      }
    }
  }

  /**
   * 该牌桌游戏结束,查叫、发送结算消息 .
   * 
   * @param table .
   */
  private void doGameOver(XzddTable table) {
    /*
     * 查牌 .
     */
    // 需要查牌的玩家
    List<XzddSeat> notOverSeats = new ArrayList<>();
    for (XzddSeat seat : table.seats) {
      if (seat.getPlayerId() > 0 && !seat.over) {
        notOverSeats.add(seat);
      }
    }
    if (!table.hasMj() && notOverSeats.size() > 1) {
      for (XzddSeat notOverSeat : notOverSeats) {
        // 结算查牌
        biller.billChaPai(notOverSeat);
      }
    }

    // 发送游戏结束消息
    msgMgr.sendGameOver(table);
    resetGame(table);

    LOG.info("[血战到底]桌子[{}]游戏结束", table.getId());
  }

  /**
   * 重置游戏 .
   * 
   * @param table .
   */
  private void resetGame(XzddTable table) {
    int readyTime = timeDic.map().get(1).getTime();
    for (XzddSeat s : table.seats) {
      s.reset();
      if (s.getPlayerId() > 0) {
        // 游戏结束后没有在指定时间内准备好，则退出座子
        s.stepFuture = scheduleMgr.schedule(() -> doExitTabl(s), readyTime + ADDED_DELAY,
            TimeUnit.SECONDS, gameExecutor);
        // 触发房间大厅成员信息变更事件
        eventMgr.post(new RoomMemInfoUpdateEvent(playerMgr.getPlayer(s.getPlayerId())));
      }
    }

    table.reset();
  }

  /**
   * 清除玩家,服务器出现bug，玩家卡在游戏中，后台可以清除玩家 .
   * 
   * @param playerId 玩家id.
   */
  private void doClearGameData(long playerId) {
    try {
      dataMgr.removePlayerSeat(playerId);
      /*
       * 清除玩家房间数据 .
       */
      for (XzddRoom room : dataMgr.rooms()) {
        room.getPlayers().remove(playerId);

        /*
         * 清除房间中玩家所在桌子数据 .
         */
        for (XzddTable table : room.tables.values()) {

          // 是否在桌子中
          boolean inTable = false;
          for (XzddSeat s : table.seats) {
            if (s.getPlayerId() > 0 && s.getPlayerId() == playerId) {
              inTable = true;
              break;
            }
          }

          if (inTable) { // 在桌子中清除数据
            for (XzddSeat s : table.seats) {
              if (s.getPlayerId() > 0) {
                dataMgr.removePlayerSeat(s.getPlayerId());
                room.getPlayers().remove(s.getPlayerId());

                /*
                 * 清除座位数据 .
                 */
                s.clear();
                Player player = playerMgr.getPlayer(s.getPlayerId());
                if (player != null) {
                  loginMgr.noticeLogoutForGameExit(player);
                }
              }
            }
          }
        }
      }

      Player player = playerMgr.getPlayer(playerId);
      if (player != null) {
        loginMgr.noticeLogoutForGameExit(player);
      }
    } catch (Exception e) {
      LOG.error("[血战到底]清除玩家[" + playerId + "]卡限失败", e);
    }
  }
}
